package com.appspot.bibtable.servlet;

import java.io.IOException;
import java.io.PrintWriter;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.logging.Logger;

import javax.jdo.JDOUserCallbackException;
import javax.jdo.PersistenceManager;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import com.appspot.bibtable.PMF;
import com.appspot.bibtable.model.Article;
import com.appspot.bibtable.model.Book;
import com.appspot.bibtable.model.Reference;
import com.appspot.bibtable.model.ReferenceEntity;
import com.appspot.bibtable.model.UserInfo;
import com.appspot.bibtable.model.annotation.Input;
import com.appspot.bibtable.model.annotation.Prototype;
import com.appspot.bibtable.util.ReflectionUtility;
import com.appspot.bibtable.view.html.HtmlTemplate;
import com.appspot.bibtable.view.html.ReferenceInputTable;
import com.appspot.bibtable.view.html.ReferenceTable;
import com.google.appengine.api.users.UserService;
import com.google.appengine.api.users.UserServiceFactory;

public class NewItemServlet extends HttpServlet
{
	private static final Logger logger = Logger.getLogger(NewItemServlet.class.getName());

	private static class ReferenceGenerator
	{
		private static final Map<String, Constructor<? extends Reference>> constructors;
		private static final Map<String, Reference> prototypes;

		static
		{
			Map<String, Constructor<? extends Reference>> constructorMap = new HashMap<String, Constructor<? extends Reference>>();
			Map<String, Reference> prototypeMap = new HashMap<String, Reference>();

			// Does not work on Google App Engine.
			// ClassFilter filter = new ClassFilter()
			// {
			// @Override
			// public boolean accept(Class<?> c)
			// {
			// return c.getAnnotation(Prototype.class) != null &&
			// ReflectionUtility.isSubclassOf(c, Reference.class);
			// }
			// };
			// Collection<Class<?>> referenceClasses =
			// ReflectionUtility.findClasses("com.appspot.bibtable.model", filter);
			// try
			// {
			// for (Class<?> c : referenceClasses)
			// {
			// String type = c.getAnnotation(Prototype.class).value();
			// Constructor<? extends Reference> constructor = ((Class<? extends
			// Reference>)c).getConstructor();
			// constructorMap.put(type, constructor);
			// prototypeMap.put(type, constructor.newInstance());
			// }
			// }
			// catch (Exception ex)
			// {
			// logger.severe(ex.getMessage());
			// }

			// Alternative method.
			try
			{
				add(Article.class, constructorMap, prototypeMap);
				add(Book.class, constructorMap, prototypeMap);
			}
			catch (Exception ex)
			{
				logger.severe(ex.getMessage());
			}

			constructors = Collections.unmodifiableMap(constructorMap);
			prototypes = Collections.unmodifiableMap(prototypeMap);
		}

		private static void add(Class<? extends Reference> c, Map<String, Constructor<? extends Reference>> constructorMap, Map<String, Reference> prototypeMap) throws Exception
		{
			String type = c.getAnnotation(Prototype.class).value();
			Constructor<? extends Reference> constructor = ((Class<? extends Reference>)c).getConstructor();
			constructorMap.put(type, constructor);
			prototypeMap.put(type, constructor.newInstance());
		}

		private static Reference newInstance(String type)
		{
			Constructor<? extends Reference> constructor = constructors.get(type);
			Reference instance = null;
			try
			{
				instance = constructor.newInstance();
			}
			catch (Exception ex)
			{
				logger.severe(ex.getMessage());
			}
			return instance;
		}

		private static Reference getPrototype(String type)
		{
			return prototypes.get(type);
		}

		private static boolean canGenerate(String type)
		{
			return constructors.containsKey(type);
		}
	}

	@Override
	protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException
	{
		UserService service = UserServiceFactory.getUserService();
		if (UserInfo.getUserInfoOf(service.getCurrentUser()) != null)
		{
			String type = getType(request);
			if (!ReferenceGenerator.canGenerate(type))
			{
				logger.warning("Unknown type " + type + " was given.");
				response.sendError(HttpServletResponse.SC_NOT_FOUND);
				return;
			}

			response.setCharacterEncoding("UTF-8");
			PrintWriter writer = response.getWriter();

			Reference reference = ReferenceGenerator.getPrototype(type);
			ReferenceEntity entity = new ReferenceEntity();
			entity.setReference(reference);

			HtmlTemplate template = new HtmlTemplate();
			ReferenceTable table = new ReferenceInputTable();
			table.setId("reference-info");
			table.setDocumentRequired(true);

			template.setTitle("New Item");
			template.setRequest(request);
			template.startDocument(writer);
			template.startContent(writer);
			writer.write("<form action=\"" + request.getRequestURI() + "\" method=\"post\">");
			table.startTable(writer);
			table.addReference(entity, writer);
			table.endTable(writer);
			writer.write("<div id=\"item-query-buttons\">");
			writer.write("<input type=\"hidden\" name=\"redirect\" value=\"detail\" />");
			writer.write("<input type=\"reset\" />");
			writer.write("<input type=\"submit\" />");
			writer.write("</div>");
			writer.write("</form>");
			writer.println();
			template.endContent(writer);
			template.endDocument(writer);
		}
		else
		{
			logger.warning("New item request from non-registered user (" + request.getRemoteAddr() + ").");
			response.sendError(HttpServletResponse.SC_FORBIDDEN);
		}
	}

	@Override
	protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException
	{
		UserService service = UserServiceFactory.getUserService();
		if (UserInfo.getUserInfoOf(service.getCurrentUser()) != null)
		{
			String type = getType(request);
			if (!ReferenceGenerator.canGenerate(type))
			{
				logger.warning("Unknown type " + type + " was given.");
				response.sendError(HttpServletResponse.SC_NOT_FOUND);
				return;
			}

			Reference reference = ReferenceGenerator.newInstance(type);
			Field[] fields = ReflectionUtility.getAllFields(reference.getClass(), true);
			for (Field field : fields)
			{
				Input input = field.getAnnotation(Input.class);
				if (input == null)
				{
					continue;
				}
				String value = request.getParameter(field.getName());
				if (value != null)
				{
					reference.setValue(field, value);
				}
			}

			ReferenceEntity entity = new ReferenceEntity();
			entity.setReference(reference);

			Field[] entityFields = ReflectionUtility.getAllFields(ReferenceEntity.class, false);
			for (Field field : entityFields)
			{
				Input input = field.getAnnotation(Input.class);
				if (input == null)
				{
					continue;
				}
				String value = request.getParameter(field.getName());
				if (value != null)
				{
					entity.setValue(field, value);
				}
			}

			PersistenceManager manager = PMF.get().getPersistenceManager();
			String key = null;
			try
			{
				key = manager.makePersistent(entity).getKeyString();
				logger.info("Entity added (key:" + key + ").");
			}
			catch (JDOUserCallbackException ex)
			{
				logger.warning(ex.getMessage());
			}
			finally
			{
				manager.close();
			}

			String redirect = request.getParameter("redirect");
			if (redirect != null)
			{
				response.sendRedirect("/" + redirect + "/" + key);
			}
		}
		else
		{
			logger.warning("New item request from non-registered user (" + request.getRemoteAddr() + ").");
			response.sendError(HttpServletResponse.SC_FORBIDDEN);
		}
	}

	private static String getType(HttpServletRequest request)
	{
		String[] path = request.getRequestURI().substring(1).split("/");
		if (path.length == 1)
		{
			logger.severe("Illegal request URI (contains only one slash).");
			return null;
		}
		return path[1];
	}
}
